"""
.. moduleauthor:: Riley Baird <rbaird@odot.org>
.. moduleauthor:: Emma Baker <ebaker@odot.org>
.. modulecreated: August 19, 2024

Provides container types for specific situations. :class:`IterableNamespace` is
similar to a ``dict`` with ``str`` keys whose elements may also be accessed as
attributes. :class:`FrozenDict` and :class:`FrozenList` are intended for use as
hashable alternatives to ``dict`` and ``list``, respectively.
"""
from itertools import chain
from types import SimpleNamespace
from typing import Optional, TypeVar, Generic, overload
from collections.abc import Mapping, MutableMapping, KeysView, ValuesView, ItemsView, Iterator, Hashable, Sequence, Iterable, Sized

import attrs

T = TypeVar("T")


class IterableNamespace(SimpleNamespace, Generic[T], MutableMapping[str, T], Sized):
    """Mutable mapping that provides both ``dict``-like and attribute-based
    element access."""

    def __contains__(self, __key: str) -> bool:
        if attrs.has(self.__class__):
            # noinspection PyTypeChecker, PyDataclass
            return __key in attrs.asdict(self, recurse=False)
        else:
            return __key in self.__dict__

    def __getitem__(self, __key: str) -> T:
        return self.__getattribute__(__key)

    def __setitem__(self, __key: str, __value: T) -> None:
        self.__setattr__(__key, __value)

    def __delitem__(self, __key: str) -> None:
        self.__delattr__(__key)

    def __len__(self) -> int:
        if attrs.has(self.__class__):
            # noinspection PyTypeChecker, PyDataclass
            return len(attrs.astuple(self, recurse=False))
        else:
            return len(self.__dict__)

    def __iter__(self) -> Iterator[str]:
        if attrs.has(self.__class__):
            # noinspection PyTypeChecker, PyDataclass
            return attrs.asdict(self, recurse=False).__iter__()
        else:
            return self.__dict__.__iter__()

    def keys(self) -> KeysView[str]:
        if attrs.has(self.__class__):
            # noinspection PyTypeChecker, PyDataclass
            return attrs.asdict(self, recurse=False).keys()
        else:
            return self.__dict__.keys()

    def values(self) -> ValuesView[T]:
        if attrs.has(self.__class__):
            # noinspection PyTypeChecker, PyDataclass
            return attrs.asdict(self, recurse=False).values()
        else:
            return self.__dict__.values()

    def items(self) -> ItemsView[str, T]:
        if attrs.has(self.__class__):
            # noinspection PyTypeChecker, PyDataclass
            return attrs.asdict(self, recurse=False).items()
        else:
            return self.__dict__.items()


KT = TypeVar("KT", bound=Hashable, covariant=True)
VT = TypeVar("VT", bound=Hashable, covariant=True)

class FrozenDict(Mapping[KT, VT], Hashable):
    """Immutable mapping class for use in cases that require a hashable
    dictionary. All keys and values must also be hashable."""

    __dict: dict[KT, VT]
    __hash: int

    def __init__(self, *args, **kwargs):
        self.__dict = dict(*args, **kwargs)
        self.__hash = hash(tuple(self.__dict.items()))

    def __getitem__(self, key, /):
        return self.__dict.__getitem__(key)

    def __len__(self):
        return self.__dict.__len__()

    def __iter__(self):
        return self.__dict.__iter__()

    def __str__(self):
        return self.__dict.__str__()

    def __repr__(self):
        return f"{self.__class__.__name__}({self.__dict.__repr__()})"

    def __hash__(self) -> int:
        return self.__hash

    def __eq__(self, other):
        return self.__dict.__eq__(other)

    def __or__(self, other):
        return self.__class__(self.__dict.__or__(other))

    def __ror__(self, other):
        return self.__class__(self.__dict.__ror__(other))

    def keys(self) -> KeysView[KT]:
        return self.__dict.keys()

    def values(self) -> ValuesView[VT]:
        return self.__dict.values()

    def items(self) -> ItemsView[KT, VT]:
        return self.__dict.items()


class FrozenList(Sequence[VT], Hashable):
    """Immutable sequence class for use in cases that require a hashable
    list. All items must also be hashable."""

    __data: tuple[VT, ...]
    __hash: int

    def __init__(self, iterable: Optional[Iterable[T]] = None):
        self.__data = tuple(iterable) if iterable is not None else tuple()
        self.__hash = hash(self.__data)

    def __hash__(self) -> int:
        return self.__hash

    @overload
    def __getitem__(self, index: int) -> VT: ...

    @overload
    def __getitem__(self, index: slice) -> Sequence[VT]: ...

    def __getitem__(self, index):
        return self.__data.__getitem__(index)

    def __len__(self) -> int:
        return self.__data.__len__()

    def __str__(self) -> str:
        return self.__data.__str__()

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.__data.__repr__()})"

    def __eq__(self, other) -> bool:
        return self.__data.__eq__(other)

    def __add__(self, other):
        return self.__class__(chain(self, other))

    def __radd__(self, other):
        return self.__class__(chain(other, self))

    def __mul__(self, other):
        return self.__class__(self.__data * other)

    def __rmul__(self, other):
        return self.__class__(other * self.__data)

__all__ = ["IterableNamespace", "FrozenList", "FrozenDict"]